/*
 * Created by Christopher King
 * Copyright (C) 2007 Christopher King, All Rights Reserved.
 */

package us.cirion.adventure.ui.mobile;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.Vector;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Image;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import javax.microedition.rms.RecordEnumeration;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;
import javax.microedition.rms.RecordStoreNotFoundException;

import org.kxml2.io.KXmlParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import us.cirion.adventure.engine.AnyNoun;
import us.cirion.adventure.engine.CurrentPlayerLocation;
import us.cirion.adventure.engine.Direction;
import us.cirion.adventure.engine.Game;
import us.cirion.adventure.engine.GameDisplay;
import us.cirion.adventure.engine.ItemLocation;
import us.cirion.adventure.engine.Noun;
import us.cirion.adventure.engine.Nowhere;
import us.cirion.adventure.engine.Room;
import us.cirion.adventure.engine.ScriptAction;
import us.cirion.adventure.engine.Verb;
import us.cirion.adventure.engine.Word;
import us.cirion.adventure.engine.prerequisite.ItemExists;
import us.cirion.adventure.engine.prerequisite.ItemInInventory;
import us.cirion.adventure.engine.prerequisite.ItemInRoom;
import us.cirion.adventure.engine.prerequisite.PlayerInRoom;
import us.cirion.adventure.engine.result.DestroyItem;
import us.cirion.adventure.engine.result.DisplayInventory;
import us.cirion.adventure.engine.result.DisplayMessage;
import us.cirion.adventure.engine.result.EndGame;
import us.cirion.adventure.engine.result.LoadGame;
import us.cirion.adventure.engine.result.NewGame;
import us.cirion.adventure.engine.result.SaveGame;
import us.cirion.adventure.engine.result.SetItemLocation;
import us.cirion.adventure.engine.result.SetPlayerLocation;

/**
 * AdventureGameEngine is the primary class responsible for running the Treasure
 * Chest text adventure game engine. It marshalls and coordinates the
 * user-interface and core game components, providing the mechanism for
 * communicating between the two. It also controls data access in the game:
 * saving and loading games, starting new games, and exiting.
 * 
 * @author Chris
 */
public class AdventureGameEngine extends MIDlet implements GameDisplay,
		InputListener
{

	public static final int ENGINE_VERSION = 1;

	private Verb chosenVerb;
	private Noun chosenNoun;
	private Noun chosenTarget;

	private HUDCanvas ui;
	private Game game;
	private MainMenu mainMenu;

	private String startText;

	String gameFileLocation;

	protected void destroyApp(boolean unconditional)
			throws MIDletStateChangeException
	{
		// Currently does nothing
	}

	protected void pauseApp()
	{
		// Currently does nothing
	}

	protected void startApp() throws MIDletStateChangeException
	{
		try
		{
			ui = initializeUIFromConfiguration();
		}
		// If we fail to create the UI, there isn't anything useful to be done -
		// we can't send messages to the user. Leave before more damage is done.
		catch (BadXMLDataException bxde)
		{
			throw new MIDletStateChangeException(bxde.getMessage());
		} catch (IOException ioe)
		{
			throw new MIDletStateChangeException(ioe.getMessage());
		} catch (XmlPullParserException xppe)
		{
			throw new MIDletStateChangeException(xppe.getMessage());
		}

		// TODO: i18n
		ui.setRoomText("Loading...");
		Display.getDisplay(this).setCurrent(ui);

		game = new Game();

		boolean loadedLastSave;

		try
		{
			loadedLastSave = loadLastSave();
		} catch (RecordStoreException rse)
		{
			// No special handling needed here, we'll just load the main menu.
			loadedLastSave = false;
		}

		if (!loadedLastSave)
		{

			mainMenu = new MainMenu(this, ui);

			// TODO: i18n
			ui.addCommand(new Command("Exit", Command.EXIT, 0));

			mainMenu.displayMenu();
		}
	}

	/**
	 * Draws a text message to the screen.
	 * 
	 * @param message The text to display.
	 */
	public void displayMessage(String message)
	{
		ui.displayMessage(message);
	}

	private void updateGameState()
	{
		Room currentLocation = game.getPlayer().getCurrentLocation();
		StringBuffer displayText = new StringBuffer(currentLocation
				.getDescription());
		Enumeration items = currentLocation.getItems();
		if (items.hasMoreElements())
		{
			// TODO: i18n
			displayText.append("\nYou see:");
			while (items.hasMoreElements())
			{
				Noun item = (Noun) items.nextElement();
				displayText.append('\n');
				displayText.append(item.getDescription());
			}
		}
		ui.setRoomText(displayText.toString());
		ui.resetWords();
		Vector words = new Vector();
		insertItems(words, currentLocation.getExitDirections());
		ui.setDirections(words);
		words = new Vector();
		insertItems(words, game.getVerbs());
		ui.addWords(words);
		ui.repaint();
	}

	private void processCommand()
	{
		game.executeScriptAction(chosenVerb, chosenNoun, chosenTarget);
		chosenVerb = null;
		chosenNoun = null;
		chosenTarget = null;
		// It's possible that the game has transitioned to a new screen as a
		// result of the
		// action - for example, displaying a list of games to load.
		// Be sure that we don't stomp on the UI if that's the case.
		if (ui.getInputListener() == this)
		{
			updateGameState();
		}
	}

	private Vector insertItems(Vector destination, Enumeration source)
	{
		while (source.hasMoreElements())
		{
			destination.addElement(source.nextElement());
		}
		return destination;
	}

	private void processNouns(int inventoryAccess, boolean displayAcceptInput)
	{
		Vector nouns = new Vector();
		if (displayAcceptInput)
		{
			nouns.addElement(AcceptInput.getInstance());
		}
		if (inventoryAccess != Verb.INVENTORY_MANDATORY)
		{
			insertItems(nouns, game.getPlayer().getCurrentLocation().getItems());
		}
		if (inventoryAccess != Verb.INVENTORY_DENIED)
		{
			insertItems(nouns, game.getPlayer().getItems());
		}
		nouns.addElement(Back.getInstance());
		ui.resetWords();
		ui.addWords(nouns);
	}

	/**
	 * Reacts to a Word selected by the user. If the user has entered enough
	 * Words to construct a complete command, then it is executed. Otherwise,
	 * record the entries so far and present the user with the next
	 * context-appropriate collection of words.
	 * 
	 * @param word The word selected by the user. This may be a Game-related
	 *            word, such as a Noun or a Verb; it also may be a
	 *            user-interface-specific word, such as Back or Done.
	 */
	public void handleWord(Word word)
	{
		// Primitive, but this fits everything we need for version 1.0 of the
		// engine.
		if (word instanceof Back)
		{
			if (chosenNoun != null)
			{
				chosenNoun = null;
				processNouns(chosenVerb.getObjectInventoryAccess(), chosenVerb
						.takesObject() == Verb.NOUN_OPTIONAL);
			} else if (chosenVerb != null)
			{
				chosenVerb = null;
				updateGameState();
			}
		} else if (word instanceof AcceptInput)
		{
			processCommand();
		} else if (word instanceof Verb)
		{
			chosenVerb = (Verb) word;
			if (chosenVerb.takesObject() == Verb.NOUN_DENIED)
			{
				processCommand();
			} else
			{
				processNouns(chosenVerb.getObjectInventoryAccess(), chosenVerb
						.takesObject() == Verb.NOUN_OPTIONAL);
			}
		} else if (chosenNoun == null)
		{
			chosenNoun = (Noun) word;
			if (chosenVerb.takesTarget() == Verb.NOUN_DENIED)
			{
				processCommand();
			} else
			{
				processNouns(chosenVerb.getTargetInventoryAccess(), chosenVerb
						.takesTarget() == Verb.NOUN_OPTIONAL);
			}
		} else
		{
			chosenTarget = (Noun) word;
			processCommand();
		}
		ui.repaint();
	}

	// TODO: Feels like this should go in another class, but I'm not sure what
	// such a beast would appropriately be named.
	private HUDCanvas initializeUIFromConfiguration()
			throws XmlPullParserException, BadXMLDataException, IOException
	{
		HUDCanvas ui = null;
		String xmlFile = "/default-ui-config.xml";
		KXmlParser parser = new KXmlParser();
		InputStream stream = getClass().getResourceAsStream(xmlFile);
		InputStreamReader reader = new InputStreamReader(stream);
		parser.setInput(reader);

		ui = readXMLConfigData(parser);
		return ui;
	}

	private void initializeGameFromFile(Game game, String fileLocation)
			throws XmlPullParserException, BadXMLDataException, IOException
	{
		this.gameFileLocation = fileLocation;
		// In version 1.0, can only read games that ship in the JAR. In the
		// future, will also be able to read from local storage or OTA.
		if (fileLocation.startsWith("jar:"))
		{
			KXmlParser parser = new KXmlParser();
			InputStream stream = getClass().getResourceAsStream(
					fileLocation.substring(fileLocation.indexOf(':') + 1));
			InputStreamReader reader = new InputStreamReader(stream);
			parser.setInput(reader);

			readXMLGameData(game, parser);
		} else
		{
			throw new IOException("Unrecognized game location " + fileLocation);
		}
	}

	// Helper function which treats the attribute as required and throws an
	// appropriate exception if it is missing.
	private String readAttribute(KXmlParser parser, String attribute)
			throws BadXMLDataException
	{
		String value = parser.getAttributeValue(null, attribute);
		if (value == null)
		{
			throw new BadXMLDataException(parser.getLineNumber(), parser
					.getName(), "Unable to read attribute " + attribute);
		}
		return value;
	}

	private int readInt(KXmlParser parser, String attribute)
			throws BadXMLDataException
	{
		String textValue = readAttribute(parser, attribute);
		try
		{
			return Integer.parseInt(textValue);
		} catch (NumberFormatException nfe)
		{
			throw new BadXMLDataException(parser.getLineNumber(), parser
					.getName(), "Could not convert " + textValue
					+ " to a number.");
		}
	}

	private Noun readNoun(KXmlParser parser, String attribute)
			throws BadXMLDataException
	{
		String targetName = readAttribute(parser, attribute);
		if (targetName.equalsIgnoreCase(Game.ANY_ITEM_STRING))
		{
			return AnyNoun.getInstance(game);
		}
		Noun target = game.getNoun(targetName);
		if (target == null)
		{
			throw new BadXMLDataException(parser.getLineNumber(), targetName,
					"Noun was not defined.");
		}
		return target;
	}

	private Room readRoom(KXmlParser parser, String attribute)
			throws BadXMLDataException
	{
		String targetName = readAttribute(parser, attribute);
		Room target = game.getRoom(targetName);
		if (target == null)
		{
			throw new BadXMLDataException(parser.getLineNumber(), targetName,
					"Room was not defined.");
		}
		return target;
	}

	private ItemLocation readItemLocation(KXmlParser parser, String attribute)
			throws BadXMLDataException
	{
		ItemLocation target = null;
		String targetName = readAttribute(parser, attribute);
		// Special case: "Inventory" moves it to the player.
		if (targetName.equalsIgnoreCase(Game.INVENTORY_STRING))
		{
			target = game.getPlayer();
		} else if (targetName.equalsIgnoreCase(Game.CURRENT_ROOM_STRING))
		{
			// I'm tempted to make CurrentPlayerLocation a singleton. The only
			// reason I haven't yet is because future versions of the engine may
			// support multiple Players - like a party-based RPG.
			target = new CurrentPlayerLocation(game.getPlayer());
		} else
		{
			target = game.getRoom(targetName);
		}
		if (target == null)
		{
			throw new BadXMLDataException(parser.getLineNumber(), targetName,
					"Item location was not found.");
		}
		return target;
	}

	private int readInventoryAccess(KXmlParser parser)
			throws BadXMLDataException
	{
		String inventoryAccess = parser.getAttributeValue(null,
				Game.INVENTORY_STRING);
		if (inventoryAccess == null
				|| inventoryAccess.equalsIgnoreCase("denied"))
		{
			return Verb.INVENTORY_DENIED;
		} else if (inventoryAccess.equalsIgnoreCase("optional"))
		{
			return Verb.INVENTORY_OPTIONAL;
		} else if (inventoryAccess.equalsIgnoreCase("mandatory"))
		{
			return Verb.INVENTORY_MANDATORY;
		}
		throw new BadXMLDataException(
				parser.getLineNumber(),
				"inventory must be empty or one of 'denied', 'optional', or 'mandatory'.  Read "
						+ inventoryAccess);
	}

	private int readNounAcceptance(KXmlParser parser)
			throws BadXMLDataException
	{
		String takesNounText = parser.getAttributeValue(null, "noun");
		if (takesNounText == null
				|| takesNounText.equalsIgnoreCase("mandatory"))
		{
			return Verb.NOUN_MANDATORY;
		} else if (takesNounText.equalsIgnoreCase("optional"))
		{
			return Verb.NOUN_OPTIONAL;
		} else if (takesNounText.equalsIgnoreCase("denied"))
		{
			return Verb.NOUN_DENIED;
		}
		throw new BadXMLDataException(parser.getLineNumber(),
				"noun must be empty or one of 'denied', 'optional', or 'mandatory'.  Read "
						+ takesNounText);
	}

	// This currently uses rather dumb parsing - we just go off tag names, not
	// their hierarchal position. In the future, maybe we should validate
	// against
	// an xsd prior to processing here.
	private HUDCanvas readXMLConfigData(KXmlParser parser) throws IOException,
			XmlPullParserException, BadXMLDataException
	{
		int eventType = parser.getEventType();
		int x, y, width, height, cardinal;
		x = y = width = height = cardinal = 0;
		HUDCanvas ui = new HUDCanvas(this, this);
		while (eventType != XmlPullParser.END_DOCUMENT)
		{
			if (eventType == XmlPullParser.START_TAG)
			{
				if (parser.getName().equals("textWindow"))
				{
					x = readInt(parser, "x");
					y = readInt(parser, "y");
					width = readInt(parser, "width");
					height = readInt(parser, "height");
					ui.configureTextWindow(x, y, width, height);
				} else if (parser.getName().equals("button"))
				{
					x = readInt(parser, "x");
					y = readInt(parser, "y");
					width = readInt(parser, "width");
					height = readInt(parser, "height");
					ui.addButtonLayout(x, y, width, height);
				} else if (parser.getName().equals("skin"))
				{
					String skinFileName = readAttribute(parser, "resource");
					InputStream stream = getClass().getResourceAsStream(
							skinFileName);
					if (stream == null)
					{
						throw new BadXMLDataException(parser.getLineNumber(),
								parser.getName(), "Could not load resource "
										+ skinFileName);
					}
					ui.setBackgroundImage(Image.createImage(stream));
				} else if (parser.getName().equals("cardinal"))
				{
					x = readInt(parser, "x");
					y = readInt(parser, "y");
					cardinal = Direction.getCardinalInt(readAttribute(parser,
							"type"));
					ui.addCardinalLayout(cardinal, x, y);
				}
			}
			eventType = parser.next();
		}
		return ui;
	}

	private HUDCanvas readXMLGameData(Game game, KXmlParser parser)
			throws IOException, XmlPullParserException, BadXMLDataException
	{
		int eventType = parser.getEventType();
		Room currentRoom = null;
		String startRoom = null;
		Verb currentVerb = null;
		ScriptAction currentScriptAction = null;

		// Some book-keeping first.
		game.addNoun(AnyNoun.getInstance(game));

		while (eventType != XmlPullParser.END_DOCUMENT)
		{
			if (eventType == XmlPullParser.START_DOCUMENT)
			{
				System.out.println("Start document");
			} else if (eventType == XmlPullParser.END_DOCUMENT)
			{
				System.out.println("End document");
			} else if (eventType == XmlPullParser.START_TAG)
			{
				if (parser.getName().equalsIgnoreCase("game"))
				{
					game.setID(readAttribute(parser, "id"));
					game.setVersion(readInt(parser, "version"));
				} else if (parser.getName().equalsIgnoreCase("startText"))
				{
					startText = parser.nextText();
				} else if (parser.getName().equalsIgnoreCase("verb"))
				{
					Verb verb = new Verb();
					verb.setText(readAttribute(parser, "name"));
					String displayText = parser.getAttributeValue(null,
							"displayText");
					if (displayText != null)
					{
						verb.setDisplayText(displayText);
					}
					game.addVerb(verb);
					currentVerb = verb;
				} else if (parser.getName().equalsIgnoreCase("object"))
				{
					currentVerb.setTakesObject(readNounAcceptance(parser));
					currentVerb
							.setObjectInventoryAccess(readInventoryAccess(parser));
				} else if (parser.getName().equalsIgnoreCase("target"))
				{
					currentVerb.setTakesTarget(readNounAcceptance(parser));
					currentVerb
							.setTargetInventoryAccess(readInventoryAccess(parser));
				} else if (parser.getName().equalsIgnoreCase("direction"))
				{
					Direction direction = new Direction();
					direction.setText(readAttribute(parser, "name"));
					direction.setDisplayText(readAttribute(parser,
							"displayText"));
					String cardinal = parser
							.getAttributeValue(null, "cardinal");
					if (cardinal != null)
					{
						if (cardinal.equalsIgnoreCase("up"))
						{
							direction.setCardinal(Direction.UP);
						} else if (cardinal.equalsIgnoreCase("down"))
						{
							direction.setCardinal(Direction.DOWN);
						} else if (cardinal.equalsIgnoreCase("left"))
						{
							direction.setCardinal(Direction.LEFT);
						} else if (cardinal.equalsIgnoreCase("right"))
						{
							direction.setCardinal(Direction.RIGHT);
						}
					}
					game.addDirection(direction);
				} else if (parser.getName().equalsIgnoreCase("rooms"))
				{
					startRoom = readAttribute(parser, "start");
				} else if (parser.getName().equalsIgnoreCase("room"))
				{
					String roomName = readAttribute(parser, "name");
					currentRoom = new Room();
					currentRoom.setDescription(readAttribute(parser,
							"description"));
					currentRoom.setName(roomName);
					game.addRoom(currentRoom);
					if (roomName.equalsIgnoreCase(startRoom))
					{
						game.getPlayer().setCurrentLocation(currentRoom);
					}
				} else if (parser.getName().equalsIgnoreCase("exit"))
				{
					currentRoom
							.addExit(game.getDirection(readAttribute(parser,
									"direction")), readAttribute(parser,
									"destination"));
				} else if (parser.getName().equalsIgnoreCase("noun"))
				{
					Noun noun = new Noun();
					boolean hasLocation = false;
					noun.setText(readAttribute(parser, "name"));
					noun.setDescription(new StringBuffer(readAttribute(parser,
							"description")));

					noun.setShortText(readAttribute(parser, "shortText"));
					String initialLocation = parser.getAttributeValue(null,
							"initialRoom");
					if (initialLocation != null)
					{
						Room room = readRoom(parser, "initialRoom");
						room.addItem(noun);
						hasLocation = true;
					}
					String startsInInventoryString = parser.getAttributeValue(
							null, "startsInInventory");
					if (startsInInventoryString != null
							&& startsInInventoryString.equalsIgnoreCase("true"))
					{
						game.getPlayer().addItem(noun);
						hasLocation = true;
					}
					if (!hasLocation)
					{
						noun.setLocation(Nowhere.getInstance());
					}
					game.addNoun(noun);
				} else if (parser.getName().equalsIgnoreCase("scriptAction"))
				{
					currentScriptAction = new ScriptAction();
					String verbName = readAttribute(parser, "verb");
					Verb verb = game.getVerb(verbName);
					if (verb == null)
					{
						throw new BadXMLDataException(parser.getLineNumber(),
								verbName,
								"Verb has not been defined.  Verbs are case-sensitive.");
					}
					currentScriptAction.setVerb(verb);
					String noun = parser.getAttributeValue(null, "noun");
					if (noun != null)
					{
						currentScriptAction.setNoun(game.getNoun(noun));
					}
					String target = parser.getAttributeValue(null, "target");
					if (target != null)
					{
						currentScriptAction.setTarget(game.getNoun(target));
					}
					game.addScriptAction(currentScriptAction);
				}

				// ////////////////
				// Pre-requisites.
				// ////////////////
				else if (parser.getName().equalsIgnoreCase("ItemInInventory"))
				{
					ItemInInventory preReq = new ItemInInventory(game
							.getPlayer(), readNoun(parser, "item"));
					currentScriptAction.addPreRequisite(preReq);
				} else if (parser.getName().equalsIgnoreCase("ItemInRoom"))
				{
					ItemInRoom preReq = new ItemInRoom(
							readNoun(parser, "item"), readRoom(parser,
									"location"));
					currentScriptAction.addPreRequisite(preReq);
				} else if (parser.getName().equalsIgnoreCase("ItemExists"))
				{
					ItemExists preReq = new ItemExists(readNoun(parser, "item"));
					currentScriptAction.addPreRequisite(preReq);
				} else if (parser.getName().equalsIgnoreCase("PlayerInRoom"))
				{
					PlayerInRoom preReq = new PlayerInRoom(game.getPlayer(),
							readRoom(parser, "location"));
					currentScriptAction.addPreRequisite(preReq);
				}

				// ////////
				// Effects
				// ////////
				else if (parser.getName().equalsIgnoreCase("Message"))
				{
					DisplayMessage result = new DisplayMessage(this,
							readAttribute(parser, "text"));
					currentScriptAction.addResult(result);
				} else if (parser.getName().equalsIgnoreCase("DestroyItem"))
				{
					DestroyItem result = new DestroyItem(readNoun(parser,
							"item"));
					currentScriptAction.addResult(result);
				} else if (parser.getName().equalsIgnoreCase("SetItemLocation"))
				{
					SetItemLocation result = new SetItemLocation(readNoun(
							parser, "item"), readItemLocation(parser,
							"newLocation"));
					currentScriptAction.addResult(result);
				} else if (parser.getName().equalsIgnoreCase(
						"SetPlayerLocation"))
				{
					SetPlayerLocation result = new SetPlayerLocation(game
							.getPlayer(), readRoom(parser, "newLocation"));
					currentScriptAction.addResult(result);
				} else if (parser.getName()
						.equalsIgnoreCase("DisplayInventory"))
				{
					DisplayInventory result = new DisplayInventory(this, game
							.getPlayer());
					currentScriptAction.addResult(result);
				} else if (parser.getName().equalsIgnoreCase("EndGame"))
				{
					EndGame result = new EndGame(this);
					currentScriptAction.addResult(result);
				} else if (parser.getName().equalsIgnoreCase("SaveGame"))
				{
					SaveGame result = new SaveGame(this);
					currentScriptAction.addResult(result);
				} else if (parser.getName().equalsIgnoreCase("LoadGame"))
				{
					LoadGame result = new LoadGame(this);
					currentScriptAction.addResult(result);
				} else if (parser.getName().equalsIgnoreCase("NewGame"))
				{
					NewGame result = new NewGame(this);
					currentScriptAction.addResult(result);
				}

			}
			eventType = parser.next();
		}
		return ui;
	}

	/**
	 * Exits the game.
	 */
	public void endGame()
	{
		// We don't want to synchronously destroy ourselves, because the "End
		// Game" result is most likely paired with other actions, such as
		// displaying a message. Notify the UI that we have been notified of the
		// game ending so it can go away at the appropriate time.
		ui.setExitRequested(true);
	}

	/**
	 * Saves current progress in the game.
	 */
	public void saveGame()
	{
		// Currently, we just use one slot. Other/later versions may provide
		// the user with choices - a menu of save game slots, or the ability to
		// enter your own save name.
		try
		{
			saveGameToSlot(0);
			// TODO: i18n
			ui.displayMessage("Game saved.");
		} catch (IOException ioe)
		{
			ui.displayMessage("Could not save game.");
		} catch (RecordStoreException rse)
		{
			ui.displayMessage("Could not save game.");
		}
	}

	private void saveGameToSlot(int slot) throws IOException,
			RecordStoreException
	{
		RecordStore store = null;
		RecordStore infoStore = null;
		RecordEnumeration re = null;
		String saveGameName = "save" + game.getID() + slot;
		try
		{
			store = RecordStore.openRecordStore(saveGameName, true);
			// Clear out the previous data.
			re = store.enumerateRecords(null, null, false);
			while (re.hasNextElement())
			{
				int id = re.nextRecordId();
				store.deleteRecord(id);
			}
			byte[] data = SaveGameRecord.getSaveGameData(game, game.getPlayer()
					.getCurrentLocation().getName(), ENGINE_VERSION);
			store.addRecord(data, 0, data.length);

			// Attempt to update the "last saved game" record. A failure here
			// isn't fatal, because this is just a convenience feature, not
			// integral to the save process.
			try
			{
				infoStore = RecordStore.openRecordStore(
						SaveGameRecord.LAST_SAVED_GAME_KEY, true);
				re = infoStore.enumerateRecords(null, null, false);
				while (re.hasNextElement())
				{
					int id = re.nextRecordId();
					infoStore.deleteRecord(id);
				}
				data = SaveGameRecord.getSaveGameRecordInfo(gameFileLocation,
						saveGameName).getBytes();
				infoStore.addRecord(data, 0, data.length);
			} catch (RecordStoreException rse)
			{
				// Do nothing.
			}
		} catch (IOException ioe)
		{
			System.err.println("I/O exception while saving game: "
					+ ioe.getMessage());
			throw ioe;
		} catch (RecordStoreException rse)
		{
			System.err.println("RSE exception while saving game: "
					+ rse.getMessage());
			throw rse;
		} finally
		{
			if (store != null)
			{
				store.closeRecordStore();
			}
			if (infoStore != null)
			{
				infoStore.closeRecordStore();
			}
		}
	}

	/**
	 * Loads a saved game, restoring the game to its previous state.
	 */
	public void loadGame()
	{
		try
		{
			loadGameFromSlot(0);
			// TODO: i18n
			ui.displayMessage("Game loaded.");
		} catch (NoSavedGameException nsge)
		{
			ui.displayMessage("No saved game found.");
		} catch (RecordStoreException rse)
		{
			ui.displayMessage("Failed to load game.");
		} catch (IOException ioe)
		{
			ui.displayMessage("Failed to load game.");
		} catch (SaveGameRecord.UnrecognizedSaveFormatException usfe)
		{
			ui.displayMessage("Unknown save game version.");
		}
	}

	/**
	 * Starts a new game. The user will have the option to switch to another
	 * game that is available.
	 */
	public void newGame()
	{
		if (mainMenu == null)
		{
			mainMenu = new MainMenu(this, ui);
		}
		mainMenu.displayMenu();
	}

	// Giving package access - other UI components can instruct the engine to
	// start a specific game.
	void newGame(String newGameName)
	{
		Verb.resetVerbOrder();
		game.reset();
		try
		{
			initializeGameFromFile(game, newGameName);
			ui.setInputListener(this);
			ui.displayMessage(startText);
			updateGameState();
		} catch (BadXMLDataException bxde)
		{
			drawGameInitializationError(bxde);
		} catch (IOException ioe)
		{
			drawGameInitializationError(ioe);
		} catch (XmlPullParserException xppe)
		{
			drawGameInitializationError(xppe);
		}
	}

	private boolean loadLastSave() throws RecordStoreException
	{
		RecordStore store = null;
		try
		{
			RecordEnumeration re = null;
			store = RecordStore.openRecordStore(
					SaveGameRecord.LAST_SAVED_GAME_KEY, false);
			if (store == null)
			{
				return false;
			}
			re = store.enumerateRecords(null, null, false);
			String saveGameRecordInfo = new String(re.nextRecord());
			String gameInfo = SaveGameRecord.getGameName(saveGameRecordInfo);
			String slotInfo = SaveGameRecord.getSlotName(saveGameRecordInfo);
			initializeGameFromFile(game, gameInfo);
			loadGameFromName(slotInfo);
			ui.setInputListener(this);
			updateGameState();
			return true;
		} catch (Exception e)
		{
			return false;
		} finally
		{
			if (store != null)
			{
				store.closeRecordStore();
			}
		}
	}

	private void loadGameFromSlot(int slot) throws IOException,
			RecordStoreException, NoSavedGameException,
			SaveGameRecord.UnrecognizedSaveFormatException
	{
		loadGameFromName("save" + game.getID() + slot);
	}

	private void loadGameFromName(String name) throws IOException,
			RecordStoreException, NoSavedGameException,
			SaveGameRecord.UnrecognizedSaveFormatException
	{
		RecordStore store = null;
		try
		{
			RecordEnumeration re = null;
			store = RecordStore.openRecordStore(name, false);
			if (store == null)
			{
				throw new NoSavedGameException();
			}
			re = store.enumerateRecords(null, null, false);
			byte[] data = re.nextRecord();
			if (data == null)
			{
				throw new NoSavedGameException();
			}
			SaveGameRecord.loadSaveGameData(data, game);
		} catch (RecordStoreNotFoundException rsnfe)
		{
			throw new NoSavedGameException();
		} catch (RecordStoreException rse)
		{
			System.err.println("RSE exception while loading game: "
					+ rse.getMessage());
			throw rse;
		} finally
		{
			if (store != null)
			{
				store.closeRecordStore();
			}
		}
	}

	private void drawGameInitializationError(Exception e)
	{
		ui.displayMessage("Unable to load game.");
		ui.repaint();
	}

	private class BadXMLDataException extends Exception
	{
		private int line;
		private String name;
		private String message;

		public BadXMLDataException(int line, String name)
		{
			this(line, name, null);
		}

		public BadXMLDataException(int line, String name, String message)
		{
			this.line = line;
			this.name = name;
			this.message = message;
		}

		public String getMessage()
		{
			String result = null;
			if (message == null)
			{
				result = "At line " + line + ": error reading element " + name;
			} else
			{
				result = "At line " + line + ": error processing element "
						+ name + ": " + message;
			}
			return result;
		}
	}

	private class NoSavedGameException extends Exception
	{
	}

}
